# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jshin

from coin.base.timestamp import get_timestamp


# https://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-whilst-preserving-order
def _remove_duplicates_f7(seq):
  seen = set()
  seen_add = seen.add
  return [x for x in seq if not (x in seen or seen_add(x))]


class CancelExecutor(object):
  def __init__(self, wo, cooldown_limiter=None):
    self._wo = wo
    self._cooldown_limiter = cooldown_limiter
    self.clear()

  def clear(self):
    self._cancel_buy_list = []
    self._cancel_sell_list = []

  def _add_to_cancel_list(self, cancel_list, order_id):
    timestamp = get_timestamp()
    if self._cooldown_limiter is None \
        or self._cooldown_limiter.add(timestamp, order_id):
      cancel_list.append(order_id)

  def remove_duplicates(self):
    self._cancel_buy_list = _remove_duplicates_f7(self._cancel_buy_list)
    self._cancel_sell_list = _remove_duplicates_f7(self._cancel_sell_list)

  def has_cancel(self):
    return len(self._cancel_buy_list) > 0 or len(self._cancel_sell_list) > 0

  def get_cancel_list(self):
    return self._cancel_buy_list + self._cancel_sell_list

  def has_buy_cancel(self):
    return len(self._cancel_buy_list) > 0

  def has_sell_cancel(self):
    return len(self._cancel_sell_list) > 0

  def cancel_buy_working_orders(self, exclude_order_id=None):
    for order in self._wo._buy_working_orders:
      if order.order_id == exclude_order_id:
        continue
      self._add_to_cancel_list(self._cancel_buy_list, order.order_id)

  def cancel_sell_working_orders(self, exclude_order_id=None):
    for order in self._wo._sell_working_orders:
      if order.order_id == exclude_order_id:
        continue
      self._add_to_cancel_list(self._cancel_sell_list, order.order_id)

  def cancel_buy_with_risk(self, pos, max_pos, max_working_qty, exclude_order_id=None):
    qty = 0
    potential_pos = pos
    for order in self._wo._buy_orders:
      qty += order.qty
      potential_pos += order.qty
      if order.order_id == exclude_order_id:
        continue
      if not order.accepted:
        continue
      # if potential_pos > max_pos or abs(qty) > max_working_qty:
      # The above line has float precision issue.
      if (potential_pos - max_pos > 1e-9 or abs(qty) - max_working_qty > 1e-9):
        self._add_to_cancel_list(self._cancel_buy_list, order.order_id)

  def cancel_sell_with_risk(self, pos, min_pos, max_working_qty, exclude_order_id=None):
    qty = 0
    potential_pos = pos
    for order in self._wo._sell_orders:
      qty += order.qty
      potential_pos -= order.qty
      if order.order_id == exclude_order_id:
        continue
      if not order.accepted:
        continue
      # if potential_pos < min_pos or abs(qty) > max_working_qty:
      # The above line has float precision issue.
      if (potential_pos - min_pos < -1e-9 or abs(qty) - max_working_qty > 1e-9):
        self._add_to_cancel_list(self._cancel_sell_list, order.order_id)

  def cancel_buy_by_price(self, pull_price, push_price, exclude_order_id=None):
    # iterate one by one and cancel
    for order in self._wo._buy_orders:
      if order.order_id == exclude_order_id:
        continue
      if not order.accepted:
        continue
      if (pull_price is not None and order.price >= pull_price) or \
          (push_price is not None and order.price <= push_price):
        self._add_to_cancel_list(self._cancel_buy_list, order.order_id)

  def cancel_sell_by_price(self, pull_price, push_price, exclude_order_id=None):
    # iterate one by one and cancel
    for order in self._wo._sell_orders:
      if order.order_id == exclude_order_id:
        continue
      if not order.accepted:
        continue
      if (pull_price is not None and order.price <= pull_price) or \
          (push_price is not None and order.price >= push_price):
        self._add_to_cancel_list(self._cancel_sell_list, order.order_id)

  def cancel_old_orders(self, timestamp_min):
    for order in self._wo._buy_orders:
      if order.accepted and \
          (order.internal.timestamp is None
           or order.internal.timestamp <= timestamp_min):
        self._add_to_cancel_list(self._cancel_buy_list, order.order_id)
    for order in self._wo._sell_orders:
      if order.accepted and \
          (order.internal.timestamp is None
           or order.internal.timestamp <= timestamp_min):
        self._add_to_cancel_list(self._cancel_sell_list, order.order_id)

  def extract_buy(self):
    if not self._wo._buy_working_orders:
      return None

    # Choose one with the biggest price
    chosen_order = max(self._wo._buy_working_orders, key=lambda order: order.price)
    if chosen_order.order_id in self._cancel_buy_list:
      self._cancel_buy_list.remove(chosen_order.order_id)
    return chosen_order

  def extract_sell(self):
    if not self._wo._sell_working_orders:
      return None

    # Choose one with the smallest price
    chosen_order = min(self._wo._sell_working_orders, key=lambda order: order.price)
    if chosen_order.order_id in self._cancel_sell_list:
      self._cancel_sell_list.remove(chosen_order)
    return chosen_order

  def cancel_outside_time_range_orders(self, timestamp_from, timestamp_to):
    for order in self._wo._buy_orders:
      tin = (order.internal.timestamp is not None and order.internal.timestamp >= timestamp_from
             and order.internal.timestamp <= timestamp_to)
      if order.accepted and not tin:
        self._add_to_cancel_list(self._cancel_buy_list, order.order_id)
    for order in self._wo._sell_orders:
      tin = (order.internal.timestamp is not None and order.internal.timestamp >= timestamp_from
             and order.internal.timestamp <= timestamp_to)
      if order.accepted and not tin:
        self._cancel_sell_list.append(order.order_id)
